#include "zbj_workflow.h"
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>

/* used to test page format. */
#define TEST_BUFF_SIZE 32
static char test_buff[TEST_BUFF_SIZE];

int validate_args(int argc, char ** argv)
{
    if (argc != 3) {
        return report_msg(0, "Usage: parser facebook.html output.json\n");
    }
    return 1;
}

int validate_file(FILE* file)
{
    fseek(file, 0L, SEEK_END);
    long file_size = ftell(file);
    if (file_size == 0) {
        return report_msg(0, "Empty file.\n");
    }

    if (file_size >= TEST_BUFF_SIZE) {
        memset(test_buff, 0, TEST_BUFF_SIZE);

        fseek(file, 0L, SEEK_SET);
        if (fread(test_buff, TEST_BUFF_SIZE - 1, 1, file) != 1) {
            return report_msg(0, "Read file content failed in load_file_to_memory.\n");
        }
        if (!strstr(test_buff, "<!DOCTYPE html>")) {
            return report_msg(0, "Not HTML5 format page.\n");
        }

        return 1;
    }
    else {
        return report_msg(0, "File is too small. Please make sure it's valid page.\n");
    }
}

void init_pagetree(PageTree* page)
{
    page->page_entry = 0;
    init_fileContents(&page->fileContent);
    page->user_name = 0;
}

int parse_filecontent(FILE * file, PageTree * page)
{
    FileContents* fileContent = &page->fileContent;
    if (!load_file_to_memory(file, fileContent)) {
        return 0;
    }

    GumboOutput* output = gumbo_parse(fileContent->contents);
    if (output) {
        page->user_name = 0;
        page->page_entry = output;
        if (check_page_type(page)) {
            return 1;
        }
        else {
            return 0;
        }
    }
    else {
        return report_msg(0, "gumbo_parse in parse_filecontent failed.\n");
    }
}

void clean_pagetree(PageTree * page)
{
    free(page->user_name);
    page->user_name = 0;
    gumbo_destroy_output(&kGumboDefaultOptions, page->page_entry);
    page->page_entry = 0;
    unload_file_from_memory(&page->fileContent);
}

int isFacebookPage(PageTree* page)
{
    /* /html/head/noscript/meta#content */
    GumboNodePtr html = page->page_entry->root;
    GumboNodePtr head, noscript;

    GumboAttribPtr id;
    if ((id = gumbo_get_attribute(&get_elem(html)->attributes, "id"))) {
        if (strcmp(id->value, "facebook") == 0) {
            if (head = get_child_node(html, GUMBO_TAG_HEAD, 0, 0)) {
                if (noscript = get_child_node(head, GUMBO_TAG_NOSCRIPT, 0, 0)) {
                    const char* content = strstr(get_elem(noscript)->original_tag.data, "URL=");
                    if (content) {
                        const char* p, *q;
                        p = strchr(content, '/');
                        if (p && (q = strchr(p + 1, '?')) && (q > p + 1)) {
                            if (isalpha(*(q - 1))) {
                                page->page_type = eFacebookListPage;
                            }
                            else {
                                page->page_type = eFacebookCommentPage;

                                q = strchr(p + 1, '/');
                            }

                            int username_length = q - p - 1;
                            page->user_name = malloc(username_length + 1);
                            memcpy(page->user_name, p + 1, username_length);
                            page->user_name[username_length] = 0;

                            return 1;
                        }
                    }
                }
            }
        }
        else {
            return 0;
        }
    }

    return 0;
}

int isYoutubePage(PageTree* page)
{
    /* /html/head/link#rel='canonical' */
    GumboNodePtr html = page->page_entry->root;
    GumboNodePtr head, link;

    if (head = get_child_node(html, GUMBO_TAG_HEAD, 0, 0)) {
        if (link = get_child_node(head, GUMBO_TAG_LINK, "rel", "canonical")) {
            GumboAttribPtr attrib = gumbo_get_attribute(&get_elem(link)->attributes, "href");
            if (attrib && strstr(attrib->value, "youtube.com")) {
                if (strstr(attrib->value, "user/")) {
                    page->page_type = eYoutubeListPage;

                    const char* name = strrchr(attrib->value, '/') + 1;
                    int name_len = strlen(name);
                    page->user_name = malloc(name_len + 1);
                    memcpy(page->user_name, name, name_len);
                    page->user_name[name_len] = 0;
                }
                else {
                    page->page_type = eYoutubeCommentPage;
                }

                return 1;
            }
        }
    }
    return 0;
}

int isTwitterPage(PageTree* page)
{
    /* /html/head/link#rel='canonical' */
    GumboNodePtr html = page->page_entry->root;
    GumboNodePtr head, link;

    if (head = get_child_node(html, GUMBO_TAG_HEAD, 0, 0)) {
        if (link = get_child_node(head, GUMBO_TAG_LINK, "rel", "canonical")) {
            GumboAttribPtr attrib = gumbo_get_attribute(&get_elem(link)->attributes, "href");
            const char* host = 0;
            if (attrib && (host = strstr(attrib->value, "twitter.com"))) {
                const char* first, *last;
                first = strchr(host, '/');
                last = strrchr(host, '/');
                if (first == last) {
                    page->page_type = eTwitterListPage;

                    const char* name = first + 1;
                    int name_len = strlen(name);
                    page->user_name = malloc(name_len + 1);
                    memcpy(page->user_name, name, name_len);
                    page->user_name[name_len] = 0;
                }
                else {
                    page->page_type = eTwitterCommentPage;

                    const char* name_start = first + 1;
                    const char* name_end = strchr(name_start, '/');
                    int name_len = name_end - name_start;
                    page->user_name = malloc(name_len + 1);
                    memcpy(page->user_name, name_start, name_len);
                    page->user_name[name_len] = 0;
                }

                return 1;
            }
        }
    }

    return 0;
}

int check_page_type(PageTree* page)
{
    assert(page->page_entry);
    GumboNodePtr root = page->page_entry->root;
    assert(root->type == GUMBO_NODE_ELEMENT);
    assert(root->v.element.tag == GUMBO_TAG_HTML);

    if (isFacebookPage(page) ||
        isYoutubePage(page) ||
        isTwitterPage(page)) {
        return 1;
    }
    else {
        page->page_type = eUnknownPage;
        return report_msg(0, "Page type unknown.\n");
    }
}

void facebook_listpage_pattern(PageTree* page, CharBuf* buf)
{
    append_str_to_charbuf(buf, "/");
    append_str_to_charbuf(buf, page->user_name);
    append_str_to_charbuf(buf, "/(photos/|/videos/|posts/)[a-zA-Z0-9]{1,}[^?\"]*");
    nullterminate_charbuf(buf);
}

int output_facebook_listpage_json(PageTree* page, jsonPtr* json)
{
    jsonPtr root = cJSON_CreateObject();
    jsonPtr url_value = cJSON_CreateArray();
    cJSON_AddItemToObject(root, "Url", url_value);
    *json = root;

    /* retrieve information */
    reg_context reg;
    CharBuf pbub;
    init_charbuf(&pbub, 128, 16);
    facebook_listpage_pattern(page, &pbub);
    int cflags = REG_EXTENDED | REG_ICASE;
    int eflags = 0;
    int retcode = setup_reg_context(&reg, pbub.buf, cflags, page->fileContent.contents, page->fileContent.file_size, eflags);
    if (retcode) {
        free_charbuf(&pbub);
        return report_msg(0, "Process facebook listpage failed.\n");
    }
    else {
        CharBuf cb;
        init_charbuf(&cb, 512, 8);

        for (retcode = regexec_next(&reg); !retcode; retcode = regexec_next(&reg)) {
            if (reg.nmatch) {
                clear_charbuf(&cb);
                append_str_to_charbuf(&cb, "https://www.facebook.com");
                append_substr_to_charbuf(&cb, reg.last_pos, reg.pm[0].rm_so, reg.pm[0].rm_eo);

                bool exist = false;
                int count = cJSON_GetArraySize(url_value);
                for (int a=0; a!=count; a++) {
                    if (!strcmp(cJSON_GetArrayItem(url_value, a)->valuestring, cb.buf)) {
                        exist = true;
                        break;
                    }
                }

                if (!exist) {
                    jsonPtr str = cJSON_CreateString(cb.buf);
                    cJSON_AddItemToArray(url_value, str);
                }
            }
        }

        free_charbuf(&cb);
        destroy_reg_context(&reg);
        free_charbuf(&pbub);
        return 1;
    }
}

void get_fb_Account(PageTree* page, jsonPtr fb_value)
{
    cJSON_ReplaceItemInObject(fb_value, "Account", cJSON_CreateString(page->user_name));
}
void get_fb_PublishDateTime(PageTree* page, jsonPtr fb_value)
{
    int retcode;
    reg_context reg;
    const char* pattern = "<span class=\"timestampContent\">([^<]*)</span>";
    retcode = setup_reg_context(&reg, pattern, REG_EXTENDED | REG_ICASE, page->fileContent.contents, page->fileContent.file_size, 0);
    if (retcode) {
        report_msg(0, "Process facebook comment page PublishDateTime failed.\n");
    }
    else {
        CharBuf mbuf;
        init_charbuf(&mbuf, 32, 8);

        retcode = regexec_next(&reg);
        if (!retcode && reg.nmatch == 2) {
            append_substr_to_charbuf(&mbuf, reg.last_pos, reg.pm[1].rm_so, reg.pm[1].rm_eo);
            cJSON_ReplaceItemInObject(fb_value, "PublishDateTime", cJSON_CreateString(mbuf.buf));
            free_charbuf(&mbuf);
            destroy_reg_context(&reg);
        }
        else {
            free_charbuf(&mbuf);
            destroy_reg_context(&reg);
            report_msg(0, "Matched more than one. It's incorrect!\n");
            return;
        }
    }
}
void get_fb_Content(PageTree* page, jsonPtr fb_value)
{
    int retcode;
    reg_context reg;
    const char* pattern = "<div class=\"_5pcb\">(.*?)</div>";
    retcode = setup_reg_context(&reg, pattern, REG_EXTENDED | REG_ICASE | REG_NEWLINE, page->fileContent.contents, page->fileContent.file_size, 0);
    if (retcode) {
        report_msg(0, "Process facebook comment page Content failed.\n");
    }
    else {
        CharBuf mbuf;
        init_charbuf(&mbuf, 512, 8);

        retcode = regexec_next(&reg);
        if (!retcode && reg.nmatch == 2) {
            append_substr_to_charbuf(&mbuf, reg.last_pos, reg.pm[1].rm_so, reg.pm[1].rm_eo);
            GumboOutput* contentTree = gumbo_parse(mbuf.buf);
            if (contentTree) {
                GumboNodePtr div = contentTree->root;  /* <div> */
                div = get_child_node_recursive(div, GUMBO_TAG_DIV, "class", "_5pbx userContent");
                if (div) {
                    GumboNodePtr p = get_child_node(div, GUMBO_TAG_P, 0,0);
                    if (p) {
                        GumboElemPtr paragraph = get_elem(p);
                        if (paragraph) {
                            CharBuf buf;
                            init_charbuf(&buf, 128, 8);
                            for (int i=0; i<paragraph->children.length; i++) {
                                GumboNodePtr n = paragraph->children.data[i];
                                switch (n->type) {
                                    case GUMBO_NODE_TEXT:
                                        append_str_to_charbuf(&buf, n->v.text.text);
                                        break;
                                    default:
                                    break;
                                }
                            }
                            if (!isempty_charbuf(&buf)) {
                                cJSON_ReplaceItemInObject(fb_value, "Content", cJSON_CreateString(buf.buf));
                            }

                            free_charbuf(&buf);

                            /* PicLink & VideoLink */
                        }
                    }
                }
            }

            gumbo_destroy_output(&kGumboDefaultOptions, contentTree);
            free_charbuf(&mbuf);
            destroy_reg_context(&reg);
        }
        else {
            free_charbuf(&mbuf);
            destroy_reg_context(&reg);
            report_msg(0, "Matched more than one. It's incorrect!\n");
            return;
        }
    }
}
void get_fb_FeedbackTargets(PageTree* page, jsonPtr fb_value)
{
    int retcode;
    reg_context reg;
    const char* pattern = "\"feedbacktargets\"";
    retcode = setup_reg_context(&reg, pattern, REG_EXTENDED | REG_ICASE, page->fileContent.contents, page->fileContent.file_size, 0);
    if (retcode) {
        report_msg(0, "Process facebook comment page feedback failed.\n");
    }
    else {
        const char* json_start = 0;
        retcode = regexec_next(&reg);
        if (!retcode && reg.nmatch == 1) {
            for (const char* p = reg.last_pos+reg.pm[0].rm_so; p != reg.text; p--) {
                if (*p == '{') {
                    json_start = p;
                    break;
                }
            }
            destroy_reg_context(&reg);
            if (!json_start) {
                report_msg(0, "Locate feedbacktargets json string failed.\n");
                return;
            }
        }
        else {
            destroy_reg_context(&reg);
            report_msg(0, "Matched more than one. It's incorrect!\n");
            return;
        }

        jsonPtr obj = cJSON_Parse(json_start);
        jsonPtr feedback = cJSON_GetObjectItem(obj, "feedbacktargets");
        jsonPtr comments = cJSON_GetObjectItem(obj, "comments");
        jsonPtr profiles = cJSON_GetObjectItem(obj, "profiles");

        feedback = cJSON_GetArrayItem(feedback, 0);
        jsonPtr permalink = cJSON_GetObjectItem(feedback, "permalink");
        jsonPtr isshare = cJSON_GetObjectItem(feedback, "isshare");
        jsonPtr likecount = cJSON_GetObjectItem(feedback, "likecount");
        jsonPtr commentcount = cJSON_GetObjectItem(feedback, "commentcount");
        jsonPtr sharecount = cJSON_GetObjectItem(feedback, "sharecount");
        /* modify */
        cJSON_ReplaceItemInObject(fb_value, "IsForward",    cJSON_Duplicate(isshare, 1));
        cJSON_ReplaceItemInObject(fb_value, "Like",         cJSON_Duplicate(likecount, 1));
        cJSON_ReplaceItemInObject(fb_value, "Comment",      cJSON_Duplicate(commentcount, 1));
        cJSON_ReplaceItemInObject(fb_value, "Share",        cJSON_Duplicate(sharecount, 1));
        cJSON_ReplaceItemInObject(fb_value, "Link",         cJSON_Duplicate(permalink, 1));

        jsonPtr comments_array = cJSON_CreateArray();
        cJSON_ReplaceItemInObject(fb_value, "CommentList", comments_array);
        int count = cJSON_GetArraySize(comments);
        for (int i=0; i!=count; i++) {
            jsonPtr one_comment_obj = cJSON_CreateObject();
            cJSON_AddItemToArray(comments_array, one_comment_obj);

            jsonPtr comment = cJSON_GetArrayItem(comments, i);
            jsonPtr comment_content = cJSON_GetObjectItem(cJSON_GetObjectItem(comment, "body"), "text");
            jsonPtr comment_timestampe = cJSON_GetObjectItem(cJSON_GetObjectItem(comment, "timestamp"), "text");
            jsonPtr comment_likecount = cJSON_GetObjectItem(comment, "likecount");
            jsonPtr comment_author = cJSON_GetObjectItem(comment, "author");

            int profile_count = cJSON_GetArraySize(profiles);
            for (int j=0; j!=profile_count; j++) {
                jsonPtr profile = cJSON_GetArrayItem(profiles, j);
                if (!strcmp(cJSON_GetObjectItem(profile, "id")->valuestring, comment_author->valuestring)) {
                    comment_author = cJSON_GetObjectItem(profile, "name");
                    break;
                }
            }

            cJSON_AddItemToObject(one_comment_obj, "User",          cJSON_Duplicate(comment_author, 1));
            cJSON_AddItemToObject(one_comment_obj, "PostDateTime",  cJSON_Duplicate(comment_timestampe, 1));
            cJSON_AddItemToObject(one_comment_obj, "Content",       cJSON_Duplicate(comment_content, 1));
            cJSON_AddItemToObject(one_comment_obj, "like",          cJSON_Duplicate(comment_likecount, 1));
            cJSON_AddItemToObject(one_comment_obj, "Share",         cJSON_CreateString(""));
            cJSON_AddItemToObject(one_comment_obj, "CommentList",   cJSON_CreateArray());
        }

        free(obj);
    }
}
void get_fb_PicLink(PageTree* page, jsonPtr fb_value)
{
}
void get_fb_VideoLink(PageTree* page, jsonPtr fb_value)
{
}

int output_facebook_commentpage_json(PageTree* page, jsonPtr* json)
{
    jsonPtr root = cJSON_CreateObject();
    jsonPtr fb_value = cJSON_CreateObject();
    cJSON_AddItemToObject(root, "Facebook", fb_value);
    *json = root;

    cJSON_AddStringToObject(fb_value, "Account", "");
    cJSON_AddStringToObject(fb_value, "PublishDateTime", "");
    cJSON_AddStringToObject(fb_value, "Content", "");
    cJSON_AddStringToObject(fb_value, "Link", "");
    cJSON_AddStringToObject(fb_value, "PicLink", "");
    cJSON_AddStringToObject(fb_value, "VideoLink", "");
    cJSON_AddStringToObject(fb_value, "IsForward", "");
    cJSON_AddStringToObject(fb_value, "Like", "");
    cJSON_AddStringToObject(fb_value, "Comment", "");
    cJSON_AddStringToObject(fb_value, "Share", "");
    cJSON_AddStringToObject(fb_value, "CommentList", "");

    // feedbacktargets
    get_fb_Account(page, fb_value);
    get_fb_PublishDateTime(page, fb_value);
    get_fb_Content(page, fb_value);
    //get_fb_Link(page, fb_value);  // in get_fb_FeedbackTargets
    get_fb_PicLink(page, fb_value);
    get_fb_VideoLink(page, fb_value);
    //get_fb_IsForward(page, fb_value);
    //get_fb_Like(page, fb_value);
    //get_fb_Comment(page, fb_value);
    //get_fb_Share(page, fb_value);
    //get_fb_comments(page, fb_value);
    get_fb_FeedbackTargets(page, fb_value);

    return 1;
}

int output_youtube_listpage_json(PageTree* page, jsonPtr* json)
{
    jsonPtr root = cJSON_CreateObject();
    jsonPtr url_value = cJSON_CreateArray();
    cJSON_AddItemToObject(root, "Url", url_value);
    *json = root;

    /* retrieve information */
    reg_context reg;
    const char* pattern = "/watch\\?v=[^\"]+";
    int cflags = REG_EXTENDED | REG_ICASE;
    int eflags = 0;
    int retcode = setup_reg_context(&reg, pattern, cflags, page->fileContent.contents, page->fileContent.file_size, eflags);
    if (retcode) {
        return report_msg(0, "Process youtube listpage failed.\n");
    }
    else {
        CharBuf cb;
        init_charbuf(&cb, 128, 8);

        for (retcode = regexec_next(&reg); !retcode; retcode = regexec_next(&reg)) {
            if (reg.nmatch) {
                clear_charbuf(&cb);
                append_str_to_charbuf(&cb, "https://www.youtube.com");
                append_substr_to_charbuf(&cb, reg.last_pos, reg.pm[0].rm_so, reg.pm[0].rm_eo);

                int exist = 0;
                int count = cJSON_GetArraySize(url_value);
                for (int a=0; a!=count; a++) {
                    if (!strcmp(cJSON_GetArrayItem(url_value, a)->valuestring, cb.buf)) {
                        exist = 1;
                        break;
                    }
                }

                if (!exist) {
                    jsonPtr str = cJSON_CreateString(cb.buf);
                    cJSON_AddItemToArray(url_value, str);
                }
            }
        }

        free_charbuf(&cb);
        destroy_reg_context(&reg);
        return 1;
    }
}

int output_youtube_commentpage_json(PageTree* page, jsonPtr* json)
{
    return 0;
}

int output_twitter_listpage_json(PageTree* page, jsonPtr* json)
{
    return 0;
}

int output_twitter_commentpage_json(PageTree* page, jsonPtr* json)
{
    return 0;
}

int output_json(PageTree* page, jsonPtr* json)
{
    switch (page->page_type) {
    case eFacebookListPage:
        return output_facebook_listpage_json(page, json);
    case eFacebookCommentPage:
        return output_facebook_commentpage_json(page, json);
    case eYoutubeListPage:
        return output_youtube_listpage_json(page, json);
    case eYoutubeCommentPage:
        return output_youtube_commentpage_json(page, json);
    case eTwitterListPage:
        return output_twitter_listpage_json(page, json);
    case eTwitterCommentPage:
        return output_twitter_commentpage_json(page, json);
    default:
        return 0;
    }
}

GumboElemPtr get_elem(GumboNodePtr node)
{
    if (node->type == GUMBO_NODE_ELEMENT) {
        return &node->v.element;
    }
    return 0;
}

GumboNodePtr get_child_node(GumboNodePtr parent, GumboTag tag, const char * attr_name, const char * attr_value)
{
    GumboElemPtr parent_elem = get_elem(parent);
    if (!parent_elem) {
        return 0;
    }

    for (int i = 0; i < parent_elem->children.length; i++) {
        GumboNodePtr p = parent_elem->children.data[i];
        if (p->type == GUMBO_NODE_ELEMENT && p->v.element.tag == tag) {
            if (attr_name == 0 || attr_value == 0) {
                return p;
            }
            for (int a = 0; a < p->v.element.attributes.length; a++) {
                GumboAttribPtr ap = p->v.element.attributes.data[a];
                if (!strcmp(ap->name, attr_name) && !strcmp(ap->value, attr_value)) {
                    return p;
                }
            }
        }
    }
    return 0;
}

GumboNodePtr get_child_node_recursive(GumboNodePtr parent, GumboTag tag, const char* attr_name, const char* attr_value)
{
    GumboElemPtr parent_elem = get_elem(parent);
    if (!parent_elem) {
        return 0;
    }

    for (int i = 0; i < parent_elem->children.length; i++) {
        GumboNodePtr p = parent_elem->children.data[i];
        if (p->type == GUMBO_NODE_ELEMENT && p->v.element.tag == tag) {
            if (attr_name == 0 || attr_value == 0) {
                return p;
            }
            for (int a = 0; a < p->v.element.attributes.length; a++) {
                GumboAttribPtr ap = p->v.element.attributes.data[a];
                if (!strcmp(ap->name, attr_name) && !strcmp(ap->value, attr_value)) {
                    return p;
                }
            }
        }
        p = get_child_node_recursive(p, tag, attr_name, attr_value);
        if (p) {
            return p;
        }
    }
    return 0;
}

//GumboElemPtr get_child_tag(GumboElemPtr parent, GumboTag tag, const char ** attr_name, const char ** attr_value, int condition_count)
//{
//    return 0;
//}


